#!/usr/bin/env python

import os
import httplib
from urllib import urlencode
import sys
from optparse import OptionParser
from urlparse import urlparse
import ssl
import logging
import getpass
from suds.client import Client, WebFault
import subprocess
import json

def readServices(path):
    try:
        result = {}
        with open(path) as ss:
            for line in ss.readlines():
                line = line.strip()
                if line:
                    i = line.index(" ")
                    key = line[:i].strip()
                    value = line[i + 1:].strip()
                    result[key] = value
        return result
    except IOError:
        return None
    
def logout(args):
    parser.set_usage("usage: %prog logout")
    parser.set_description("Logout from current IJP/ICAT session.")
    
    client = Client(icatUrl + "/ICATService/ICAT?wsdl")
    service = client.service
 
    try:
        sessionId = getSessionId()
        service.logout(sessionId)
        os.remove(os.path.join(os.environ["HOME"], ".icatsessionid"))
    except WebFault as wf:
        exc = wf.fault.detail.IcatException
        fatal(exc.type + " exception: " + exc.message)
 
def refresh(args):
    parser.set_usage("usage: %prog refresh")
    parser.set_description("Keep the current ICAT session running.")
    
    client = Client(icatUrl + "/ICATService/ICAT?wsdl")
    service = client.service
 
    try:
        sessionId = getSessionId()
        service.refresh(sessionId)
    except WebFault as wf:
        exc = wf.fault.detail.IcatException
        fatal(exc.type + " exception: " + exc.message) 

def login(args):
    parser.set_usage("usage: %prog login authenticator [credential name credential value]...") 
    parser.set_description("""The first parameter, authenticator, is always needed.
It is followed by pairs of names and values
of credentials. If '-' is used as a value you will be prompted to enter the actual value with echo
to the screen turned off. For example: '%prog login ldap username smf98 password -'""")
 
    if len(args) == 0:
        print >> sys.stderr, "Must specify authenticator followed by even number of arguments to represent name value pairs (or '-h' for help)"
        sys.exit(1)
        
    if "-h" in args or "--help" in args:
        parser.print_help()
        return
   
    logging.basicConfig(level=logging.CRITICAL)

    credentials = args
    if len(credentials) % 2 != 1:
        fatal("login subcommand requires authenticator followed by even number of arguments to represent name value pairs")
    client = Client(icatUrl + "/ICATService/ICAT?wsdl")

    service = client.service
    factory = client.factory

    credentialMap = factory.create("credentials")
    for i in range(1, len(credentials), 2):
        entry = factory.create("credentials.entry")
        entry.key = credentials[i]
        if credentials[i + 1] == "-":
            entry.value = getpass.getpass(credentials[i] + ": ")
        else:
            entry.value = credentials[i + 1]
        credentialMap.entry.append(entry)
    sessionId = service.login(credentials[0], credentialMap)

    with open(os.path.join(os.environ["HOME"], ".icatsessionid"), "w") as f:
        f.write(sessionId)

    print sessionId

def submit(args):
    parser.set_usage("usage: %prog submit job [parameter...]") 
    parser.set_description("""Submit a job """)
    parser.add_option("--interactive", help="specify the job to be interactive", action="store_true")
    parser.add_option("--family", help="specify the family to use")
 
    if len(args) == 0:
        print >> sys.stderr, "Must specify job mnemonic followed by any parameters of the job (or '-h' for help)"
        sys.exit(1)
        
    if "-h" in args or "--help" in args:
        parser.print_help()
        return
        
    parms = {}
    
    if "--interactive" in args:
        parms ["interactive"] = True
        args.remove("--interactive")
    
    if "--family" in args:
        n = args.index("-family")
        parms["family"] = args[n + 1]
        del args[n]
        del args[n]
          
    parms ["jobName"] = args[0]
    parms["parameter"] = args[1:]
          
    result = processUrl("POST", "submit", parms, return_value=True)
    if result:
        if result.startswith("rdesktop"):
            subprocess.call(result.split())
        else:
            print result

def status(args):
    parser.set_usage("usage: %prog status [jobid]")
    parser.set_description("Get an overview of all jobs or details of one job if you specify its jobid.")
    (options, args) = parser.parse_args(args)
    if len(args) > 1: fatal("status subcommand expects at most one argument")
    if len(args) == 1: 
        url = "status/" + args[0]
        job = processUrl("GET", url, {}, return_value=True)
        if job:
            job = json.loads(job)
            print job["jobId"], job["name"], job["date"], job["status"]
    else: 
        url = "status"
        jobs = processUrl("GET", url, {}, return_value=True)
        if jobs:
            jobs = json.loads(jobs)
            for job in jobs:
                print job["jobId"], job["name"], job["date"], job["status"]  
 
def output(args):
    parser.set_usage("usage: %prog output [options] jobid")
    parser.set_description("Get output from a job. You may request the error output. If the job is running it will return the output generated so far.")
    parser.add_option("-e", "--error", action="store_true", dest="error",
                  help="request stderr rather than stdout")
    (options, args) = parser.parse_args(args)
    if len(args) != 1: fatal("output subcommand must have one argument: the job id")
    if options.error: 
        url = "error/" + args[0]
    else: 
        url = "output/" + args[0]
    processUrl("GET", url, {})

def delete(args):
    parser.set_usage("usage: %prog delete [options] jobid")
    parser.set_description("""Delete all record of a job. This command is only allowed when a job is neither queued nor executing. 
    After succesful deletion no operations are possible on the job.""")
    (options, args) = parser.parse_args(args)
    if len(args) != 1: fatal("Must have one argument: the job id")
    url = "delete/" + args[0]
    processUrl("DELETE", url, {})
 
def cancel(args):
    parser.set_usage("usage: %prog cancel [options] jobid")
    parser.set_description("""Remove the job from the queue or cancel it while running. Any output will still be available 
    with the 'output' subcommand until such time as the 'delete' subcommand is used to tidy up.""")
    (options, args) = parser.parse_args(args)
    if len(args) != 1: fatal("Must have one argument: the job id")
    url = "cancel/" + args[0]
    processUrl("POST", url, {})

def jobtype(args):
    parser.set_usage("usage: %prog jobtype [options] jobid")
    parser.set_description("Get a list of available job types or details of one job type if you specify its name.")
    (options, args) = parser.parse_args(args)
    if len(args) > 1: fatal("help subcommand expects at most one argument")
    if len(args) == 1: 
        url = "jobtype/" + args[0]
    else: 
        url = "jobtype"
    processUrl("GET", url, {})
    
def session(args):
    parser.set_usage("usage: %prog session")
    parser.set_description("Reports the status of your current session.")
    
    if "-h" in args or "--help" in args:
        parser.print_help()
        return
   
    logging.basicConfig(level=logging.CRITICAL)
    client = Client(icatUrl + "/ICATService/ICAT?wsdl")
    service = client.service
    factory = client.factory

    try:
        sessionId = getSessionId()
        version = service.getApiVersion()
        userName = service.getUserName(sessionId)
        minutes = str(int(service.getRemainingMinutes(sessionId)))
        print "User", userName, "(" + sessionId + ")", "connected to ICAT", version, "at", icatUrl, "with", minutes, "minutes left."
    except WebFault as wf:
        exc = wf.fault.detail.IcatException
        fatal(exc.type + " exception: " + exc.message)
       
def help(args):
    parser.set_usage("usage: %prog subcommand [parameters...] [options...]")
    parser.set_description("Interact with the ICAT Job Portal. Subcommands are " + str(subcommands.keys()) + 
                            """ All subcommands accept a '-h' or '--help'. For example '%prog status -h'
provides brief information about the 'status' subcommand. Most commands require that you login in first 
with '%prog login'.""") 
    parser.print_help()
   
services = None
fLocal = os.path.join(os.environ["HOME"], ".icat.services")
fSystem = "/usr/local/etc/glassfish.services"
if os.path.isfile(fLocal):
    services = readServices(fLocal)
elif os.path.isfile(fSystem):
    services = readServices(fSystem)
else:
    services = {}
if "icatUrl" not in services or "ijpUrl" not in services:
    if "icatUrl" not in services: services["icatUrl"] = raw_input("Please specify the url of the icat server you wish to use in the form https://example.com:8181 ")
    if "ijpUrl" not in services: services["ijpUrl"] = raw_input("Please specify the url of the ijp server you wish to use in the form https://example.com:8181 ")
    with open (fLocal, "w") as ss:
        for key in services.keys():
            print >> ss, key, services[key]
    print "This has been recorded in", fLocal

icatUrl = services["icatUrl"]
ijpUrl = services["ijpUrl"]
ijpHost = urlparse(ijpUrl).netloc
        
subcommands = {}
subcommands["login"] = login
subcommands["status"] = status
subcommands["submit"] = submit
subcommands["output"] = output
subcommands["delete"] = delete
subcommands["cancel"] = cancel
subcommands["jobtype"] = jobtype
subcommands["-h"] = help
subcommands["--help"] = help
subcommands["session"] = session
subcommands["refresh"] = refresh
subcommands["logout"] = logout

def getSessionId():
    try:
        with open(os.path.join(os.environ["HOME"], ".icatsessionid"), "r") as f:
            sessionId = f.readline()
    except IOError:
        fatal("Please log in with '" + os.path.basename(sys.argv[0]) + " login ...'")
    return sessionId

def processUrl(method, relativeUrl, parameters, return_value=False):
    parameters["sessionId"] = getSessionId()
    path = "/ijp/rest/jm/" + relativeUrl
    if parameters: parameters = urlencode(parameters, True)
    if parameters and method != "POST":
        path = path + "?" + parameters
    conn = httplib.HTTPSConnection(ijpHost)    
    conn.putrequest(method, path, skip_accept_encoding=True)
    conn.putheader("Cache-Control", "no-cache")
    conn.putheader("Pragma", "no-cache")
    conn.putheader("Accept", "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2")
    conn.putheader("Connection", "keep-alive") 
    
    if parameters and method == "POST":
        conn.putheader('Content-Length', str(len(parameters)))
        conn.putheader('Content-Type', 'application/x-www-form-urlencoded')       
            
    conn.endheaders()
    
    if parameters and method == "POST":
        conn.send(parameters)
    
    response = conn.getresponse()
    rc = response.status
    responseString = response.read()
    conn.close()
  
    if rc / 100 == 2:
        if responseString:
            if return_value: return responseString
            else: print responseString
    else:
        print rc, responseString

def fatal(msg):
    print >> sys.stderr, msg
    sys.exit(1)

if len(sys.argv) == 1:
    fatal("First argument must be one of " + str(subcommands.keys()))

parser = OptionParser()
command = sys.argv[1]
op = subcommands.get(command)

if not op:
    fatal("First argument must be one of " + str(subcommands.keys()))

op(sys.argv[2:])
